package com.sb.registration.component.service.ds.impl;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.stereotype.Component;

import com.sb.registration.component.exceptions.DatastoreSystemException;
import com.sb.registration.component.exceptions.UserAlreadyExistsException;
import com.sb.registration.component.exceptions.UserDoesNotExistException;
import com.sb.registration.component.service.ds.UsersDataSource;
import com.sb.registration.data.jaxb.gen.Users;
import com.sb.registration.data.jaxb.gen.Users.User;

@Component
public class UsersXMLDataSourceImpl implements UsersDataSource {

	private static Log logger_ = LogFactory.getLog(UsersXMLDataSourceImpl.class);
	
	/**
	 * JaxB2 parser from spring to parse the Users XML
	 * 
	 */
	@Autowired
	@Qualifier("usersXmlParser")
	private Jaxb2Marshaller usersXmlParser;


	@Value("${application.users.datasource.xml.path}")
	private String usersXmlRepositoryPath;

	/**
	 * map to store all users available in xml repository
	 * 
	 * NOTE - THIS IS NOT BEST WAY TO KEEP ALL USERS IN MEMORY, BUT TO KEEP XML
	 * READ WRITE OPERATION MINIMUM WE USED IT. THIS CAN BE AVOIDED LATER BY
	 * USING ORM LIKE HIBERNATE WITH SOME DATABASE INSTEAD OF XML FILE. JUST FOR
	 * THIS CODING CHALLENGE I HAVE USED THIS APPROACH.
	 */
	private Map<String, User> userDataMap;	
	
	/**
	 * Cached instance of users object - replica of data stored in XML datastore
	 * NOT A PERFECT VERSION - UPDATE TO THIS INSTANCE SHOULD BE SYNCHORNIZED TO MAKE IT THREAD SAFE
	 * BETTER APPROACH WOULD BE TO NOT CACHE THIS AT THE COST OF PERFORMANCE
	 */
	private Users users;
	

	@PostConstruct
	public void setup() throws IOException, DatastoreSystemException {

		logger_.info("Initializing existing users map");
		
		File file = new File(usersXmlRepositoryPath);
		
		if(!file.exists()){
			file.createNewFile();
			writeUsersData(new Users());
		}
		
		loadUsersMap();		// loads existing users into the map
		logger_.info("Done load of users map");
	}

	/**
	 * Method to add user to XML data store
	 */
	public void addUser(User user) throws UserAlreadyExistsException, DatastoreSystemException {
		
		if(!userDataMap.containsKey(user.getEmail())){
			
			users.getUser().add(user); 							// add user to Users object
			
			try {
				writeUsersData(users);
			} catch (DatastoreSystemException e) {
				resetCachedUsersInstance(); 	//insertion fails - refresh user data as cached global variable Users users have new user already added
				throw e;
			}
			
			userDataMap.put(user.getEmail(), user);			// add user to user data map
			
		}else{
			throw new UserAlreadyExistsException("User Already Exists with Email Id " + user.getEmail());
		}
	}
	
	/**
	 * Method to update user
	 */
	public synchronized void updateUser(User user) throws DatastoreSystemException, UserDoesNotExistException{
		
		User dsUser = userDataMap.get(user.getEmail());
		
		if(null != dsUser){
			
			users.getUser().remove(dsUser);							// removing old user from list
			users.getUser().add(user);					// add updated user to list
			
			try {
				writeUsersData(users);				// persist updated values
			} catch (DatastoreSystemException e) {
				resetCachedUsersInstance(); 	//insertion fails - refresh user data as cached global variable Users users have new user already added
				throw e;
			}
			
			userDataMap.put(user.getEmail(), user);			// add updated user to user data map
			
		}else{
			throw new UserDoesNotExistException("User Already Exists with Email Id " + user.getEmail());
		}
	}
	
	/***
	 * Method to delete user based on passed instance of user object
	 */
	public void deleteUser(User user) throws DatastoreSystemException, UserDoesNotExistException{
		
		User dsUser = userDataMap.get(user.getEmail());
		
		if(null != dsUser){
			
			users.getUser().remove(dsUser);							// removing old user from list
			
			try {
				writeUsersData(users);				// persist updated values
			} catch (DatastoreSystemException e) {
				//ROLLBACK
				resetCachedUsersInstance(); 	//insertion fails - refresh user data as cached global variable Users users have new user already added
				throw e;
			}
			
			userDataMap.remove(user.getEmail());			// remove user from  user data map
			
		}else{
			throw new UserDoesNotExistException("User Already Exists with Email Id " + user.getEmail());
		}
	}
	
	/**
	 * Method to delete user based on provided email id - unique identifier
	 * 
	 * @param emailId
	 * @throws DatastoreSystemException
	 * @throws UserDoesNotExistException
	 */
	public void deleteUser(String emailId) throws DatastoreSystemException, UserDoesNotExistException{
		
		User dsUser = userDataMap.get(emailId);
		
		if(null != dsUser){
			
			users.getUser().remove(dsUser);							// removing old user from list
			
			try {
				writeUsersData(users);				// persist updated values
			} catch (DatastoreSystemException e) {
				//ROLLBACK
				resetCachedUsersInstance(); 	//insertion fails - refresh user data as cached global variable Users users have new user already added
				throw e;
			}
			
			userDataMap.remove(emailId);			// remove user from  user data map
			
		}else{
			throw new UserDoesNotExistException("User Does Not Exist with Email Id " + emailId + ", Nothing to Delete");
		}
	}
	
	/**
	 * get all users existing in XML repository
	 */
	public List<User> getAllUsers(){
		return users.getUser();
	}
	
	/**
	 * get user object based on email id - unique identifier in this case
	 */
	public User getUser(String emailId){
		
		return userDataMap.get(emailId);		
		
	}
	
	/**
	 * Method to clear all data in XML repository and reset
	 */
	public void clearAll() throws IOException, DatastoreSystemException{
		File file = new File(usersXmlRepositoryPath);
		if(file.exists()){
			file.delete();
		}
		
		setup();
	}
	
	/**
	 * Reset value of users list in cached Users object with value from map. 
	 * This is required in case any add or delete to xml datastore operation fails
	 * 
	 */
	private void resetCachedUsersInstance(){
		
		users.getUser().clear();
		users.getUser().addAll(userDataMap.values());		
		
	}
	

	private void  loadUsersMap() throws DatastoreSystemException {
		users = fetchUsersData();
		if(null != users){
			userDataMap = new HashMap<String, User>(); // initialize map
			userDataMap = Collections.synchronizedMap(userDataMap);		//getting instance of synchronized map - to make it thread safe
			for(User user:users.getUser()){
				userDataMap.put(user.getEmail(), user);
			}
		}
		
	}

	/**
	 * Utility method to close any IO Stream
	 * @param stream
	 */
	private void closeStream(Closeable stream){
		try {
			if(stream != null){
				stream.close();
			}
		} catch (IOException e) {
			//log - error while closing the stream
			e.printStackTrace();
		}
		
	}
	
	/**
	 * Method to write users data to XML data store. It uses JAXB with Spring Implementation
	 * 
	 * @param users
	 * @throws DatastoreSystemException
	 */
	private synchronized void writeUsersData(Users users) throws DatastoreSystemException{
		FileOutputStream fos = null;
		try{
		
			fos = new FileOutputStream(usersXmlRepositoryPath);
			usersXmlParser.marshal(users, new StreamResult(fos));
		}catch(Exception e){
			System.out.println("Error while creating user data file, make sure file directory exists " + usersXmlRepositoryPath);
			e.printStackTrace(); //TODO
			throw new DatastoreSystemException("Error while creating user data file, make sure file directory exists " + usersXmlRepositoryPath);
			
		}finally{
			closeStream(fos);
		}
		
	}
	
	/**
	 * Method to read users data from XML data store. It uses JAXB with Spring Implementation
	 * 
	 * @return
	 * @throws DatastoreSystemException
	 */
	private synchronized Users fetchUsersData() throws DatastoreSystemException{
		
		Source inputSource = null;
		FileInputStream fis = null;
		
		try {
			
			fis = new FileInputStream(usersXmlRepositoryPath);
							
			inputSource = new StreamSource(fis);
			users = (Users) usersXmlParser.unmarshal(inputSource);	
	
			
		} catch (Exception e) {
			e.printStackTrace();
			throw new DatastoreSystemException("Error while unmarshalling users data file", e);
		} finally {
			closeStream(fis);
		}
		
		return users;
	}

}
